// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See License.txt in the repository root.

package com.microsoft.tfs.util.xml;

import java.util.ArrayList;
import java.util.List;

import org.w3c.dom.CDATASection;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.Text;

import com.microsoft.tfs.util.Check;

/**
 * <p>
 * {@link DOMUtils} contains static helper methods for working with XML
 * documents using the DOM {@link Document} API.
 * </p>
 *
 * <p>
 * There are two categories of methods. One category is methods that add new
 * content to the document tree (<code>append*()</code>). The second category is
 * methods that retrieve existing content from the tree (<code>get*()</code>).
 * </p>
 *
 * <p>
 * Any XML-related exception generated by this class will be wrapped in an
 * unchecked {@link XMLException}. Callers do not need to handle several
 * different (often checked) JAXP-specific exceptions. If callers want to handle
 * exceptions generated by this class, they must catch {@link XMLException}.
 * </p>
 */
public class DOMUtils {
    /**
     * A DOM helper method to get the text contents of a {@link Node}. If the
     * {@link Node} is a text node ({@link Node#TEXT_NODE}) or a CDATA section
     * node ({@link Node#CDATA_SECTION_NODE}), the node's value is returned. If
     * the {@link Node} is a element node ({@link Node#ELEMENT_NODE}), the node
     * value of any <i>direct child</i> nodes that are text or CDATA sections
     * are appended together and returned. If the node is not of any of the
     * above types, an empty string is returned.
     *
     * @param node
     *        the {@link Node} to get text for (must not be <code>null</code>)
     * @return the text (never <code>null</code> but may be an empty
     *         {@link String})
     */
    public static String getText(final Node node) {
        Check.notNull(node, "node"); //$NON-NLS-1$

        final int type = node.getNodeType();

        if (Node.ELEMENT_NODE == type) {
            final NodeList children = node.getChildNodes();
            final StringBuffer buffer = new StringBuffer();
            final int length = children.getLength();
            for (int i = 0; i < length; i++) {
                final Node child = children.item(i);
                final int childType = child.getNodeType();
                if (Node.TEXT_NODE == childType || Node.CDATA_SECTION_NODE == childType) {
                    buffer.append(child.getNodeValue());
                }
            }
            return buffer.toString();
        }

        if (Node.TEXT_NODE == type || Node.CDATA_SECTION_NODE == type) {
            return node.getNodeValue();
        }

        return ""; //$NON-NLS-1$
    }

    /**
     * Walks the parent chain from the specified element to find the root node.
     *
     * @param descendant
     *        The starting point for the parent chain traversal.
     *
     * @return The root node of the document.
     */
    public static Element getRootElement(final Element descendant) {
        Element element = descendant;
        while (element.getParentNode() != null && element.getParentNode() instanceof Element) {
            element = (Element) element.getParentNode();
        }
        return element;
    }

    /**
     * Enumerates all of the child {@link Element}s of the specified
     * {@link Node}.
     *
     * @param node
     *        the {@link Node} to enumerate child {@link Element}s of (must not
     *        be <code>null</code>)
     * @return the child {@link Element}s of the specified {@link Node} (never
     *         <code>null</code>)
     */
    public static Element[] getChildElements(final Node node) {
        return getChildElementsInternal(node, false, null, null);
    }

    /**
     * Enumerates all of the child {@link Element}s of the specified
     * {@link Node} that have the specified name.
     *
     * @param tagName
     *        the child element name to match on, <code>null</code> or
     *        <code>*</code> to return child elements with any local name
     * @param node
     *        the {@link Node} to enumerate child {@link Element}s of (must not
     *        be <code>null</code>)
     * @return the child {@link Element}s that matched the specified criteria
     *         (never <code>null</code>)
     */
    public static Element[] getChildElements(final Node node, final String tagName) {
        return getChildElementsInternal(node, false, null, tagName);
    }

    /**
     * Enumerates all of the child {@link Element}s of the specified
     * {@link Node} that have the specified name.
     *
     * @param namespaceURI
     *        the child element namespace to match on, <code>null</code> to
     *        return only child elements not in a namespace, <code>*</code> to
     *        return child elements in any namespace
     * @param tagName
     *        the child element local name to match on, <code>null</code> or
     *        <code>*</code> to return child elements with any local name
     * @param node
     *        the {@link Node} to enumerate child {@link Element}s of (must not
     *        be <code>null</code>)
     * @return the child {@link Element}s that matched the specified criteria
     *         (never <code>null</code>)
     */
    public static Element[] getChildElementsNS(final Node node, final String namespaceURI, final String localName) {
        return getChildElementsInternal(node, true, namespaceURI, localName);
    }

    /**
     * Gets the first child {@link Element} of the specified {@link Node}, if
     * any.
     *
     * @param node
     *        the {@link Node} to get the first child {@link Element}s of (must
     *        not be <code>null</code>)
     * @return the first child {@link Element} of the specified {@link Node}, or
     *         <code>null</code> if there were no child elements
     */
    public static Element getFirstChildElement(final Node node) {
        return getFirstChildElementInternal(node, false, null, null);
    }

    /**
     * Gets the first child {@link Element} of the specified {@link Node} that
     * has the specified name, if any.
     *
     * @param tagName
     *        the child element name to match on, <code>null</code> or
     *        <code>*</code> to match child elements with any local name
     * @param node
     *        the {@link Node} to get the first child {@link Element} of (must
     *        not be <code>null</code>)
     * @return the first child {@link Element} that matched the specified
     *         criteria, or <code>null</code> if there were no child elements
     *         that matched
     */
    public static Element getFirstChildElement(final Node node, final String tagName) {
        return getFirstChildElementInternal(node, false, null, tagName);
    }

    /**
     * Gets the first child {@link Element} of the specified {@link Node} that
     * has the specified name, if any.
     *
     * @param namespaceURI
     *        the child element namespace to match on, <code>null</code> to
     *        match child elements not in a namespace, <code>*</code> to match
     *        child elements in any namespace
     * @param tagName
     *        the child element local name to match on, <code>null</code> or
     *        <code>*</code> to match child elements with any local name
     * @param node
     *        the {@link Node} to get the first child {@link Element} of (must
     *        not be <code>null</code>)
     * @return the first child {@link Element} that matched the specified
     *         criteria, or <code>null</code> if there were no child elements
     *         that matched
     */
    public static Element getFirstChildElementNS(final Node node, final String namespaceURI, final String localName) {
        return getFirstChildElementInternal(node, true, namespaceURI, localName);
    }

    /**
     * Adds a new {@link Element} node to a {@link Document} tree as a child of
     * the specified parent {@link Element}.
     *
     * @param parent
     *        the parent {@link Element} (must not be <code>null</code>)
     * @param tagName
     *        the name to give the new child {@link Element} (must not be
     *        <code>null</code>)
     * @return the new child {@link Element} (never <code>null</code>)
     */
    public static Element appendChild(final Element parent, final String tagName) {
        Check.notNull(parent, "parent"); //$NON-NLS-1$

        final Element newChild = parent.getOwnerDocument().createElement(tagName);
        parent.appendChild(newChild);
        return newChild;
    }

    /**
     * Adds a new {@link Element} node to a {@link Document} tree as a child of
     * the specified parent {@link Element}.
     *
     * @param parent
     *        the parent {@link Element} (must not be <code>null</code>)
     * @param namespaceURI
     *        the namespace URI to put the new child in, or <code>null</code>
     * @param qualifiedName
     *        the qualified name to give the new child (must not be
     *        <code>null</code>)
     * @return the new child {@link Element} (never <code>null</code>)
     */
    public static Element appendChildNS(final Element parent, final String namespaceURI, final String qualifiedName) {
        Check.notNull(parent, "parent"); //$NON-NLS-1$

        final Element newChild = parent.getOwnerDocument().createElementNS(namespaceURI, qualifiedName);
        parent.appendChild(newChild);
        return newChild;
    }

    /**
     * Adds a new {@link Text} node to a {@link Document} tree as a child of the
     * specified parent {@link Element}.
     *
     * @param parent
     *        the parent {@link Element} (must not be <code>null</code>)
     * @param data
     *        the contents to give the new {@link Text} node
     * @return the new {@link Text} node (never <code>null</code>)
     */
    public static Text appendText(final Element parent, final String data) {
        Check.notNull(parent, "parent"); //$NON-NLS-1$

        final Text newChild = parent.getOwnerDocument().createTextNode(data);
        parent.appendChild(newChild);
        return newChild;
    }

    /**
     * Adds a new {@link CDATASection} node to a {@link Document} tree as a
     * child of the specified parent {@link Element}.
     *
     * @param parent
     *        the parent {@link Element} (must not be <code>null</code>)
     * @param data
     *        the contents to give the new {@link CDATASection} node
     * @return the new {@link CDATASection} node (never <code>null</code>)
     */
    public static CDATASection appendCDATA(final Element parent, final String data) {
        Check.notNull(parent, "parent"); //$NON-NLS-1$

        final CDATASection newChild = parent.getOwnerDocument().createCDATASection(data);
        parent.appendChild(newChild);
        return newChild;
    }

    /**
     * Adds a new {@link Element} node to the {@link Document} tree as a child
     * of the specified parent {@link Element}, and adds a new {@link Text} node
     * to the {@link Document} tree as a child of the new {@link Element} node.
     *
     * @param parent
     *        the parent {@link Element} (must not be <code>null</code>)
     * @param tagName
     *        the name to give the new child {@link Element} (must not be
     *        <code>null</code>)
     * @param data
     *        the contents to give the new {@link Text} node
     * @return the new child {@link Element} (never <code>null</code>)
     */
    public static Element appendChildWithText(final Element parent, final String tagName, final String data) {
        Check.notNull(parent, "parent"); //$NON-NLS-1$

        final Element newChild = appendChild(parent, tagName);
        appendText(newChild, data);
        return newChild;
    }

    /**
     * Adds a new {@link Element} node to the {@link Document} tree as a child
     * of the specified parent {@link Element}, and adds a new {@link Text} node
     * to the {@link Document} tree as a child of the new {@link Element} node.
     *
     * @param parent
     *        the parent {@link Element} (must not be <code>null</code>)
     * @param namespaceURI
     *        the namespace URI to put the new child in, or <code>null</code>
     * @param qualifiedName
     *        the qualified name to give the new child (must not be
     *        <code>null</code>)
     * @param data
     *        the contents to give the new {@link Text} node
     * @return the new child {@link Element} (never <code>null</code>)
     */
    public static Element appendChildWithTextNS(
        final Element parent,
        final String namespaceURI,
        final String qualifiedName,
        final String data) {
        final Element newChild = appendChildNS(parent, namespaceURI, qualifiedName);
        appendText(newChild, data);
        return newChild;
    }

    /**
     * Adds a new {@link Element} node to the {@link Document} tree as a child
     * of the specified parent {@link Element}, and adds a new
     * {@link CDATASection} node to the {@link Document} tree as a child of the
     * new {@link Element} node.
     *
     * @param parent
     *        the parent {@link Element} (must not be <code>null</code>)
     * @param tagName
     *        the name to give the new child {@link Element} (must not be
     *        <code>null</code>)
     * @param data
     *        the contents to give the new {@link CDATASection} node
     * @return the new child {@link Element} (never <code>null</code>)
     */
    public static Element appendChildWithCDATA(final Element parent, final String tagName, final String data) {
        final Element newChild = appendChild(parent, tagName);
        appendCDATA(newChild, data);
        return newChild;
    }

    /**
     * Adds a new {@link Element} node to the {@link Document} tree as a child
     * of the specified parent {@link Element}, and adds a new
     * {@link CDATASection} node to the {@link Document} tree as a child of the
     * new {@link Element} node.
     *
     * @param parent
     *        the parent {@link Element} (must not be <code>null</code>)
     * @param namespaceURI
     *        the namespace URI to put the new child in, or <code>null</code>
     * @param qualifiedName
     *        the qualified name to give the new child (must not be
     *        <code>null</code>)
     * @param data
     *        the contents to give the new {@link CDATASection} node
     * @return the new child {@link Element} (never <code>null</code>)
     */
    public static Element appendChildWithCDATANS(
        final Element parent,
        final String namespaceURI,
        final String qualifiedName,
        final String data) {
        final Element newChild = appendChildNS(parent, namespaceURI, qualifiedName);
        appendCDATA(newChild, data);
        return newChild;
    }

    /**
     * Helper method to enumerate child {@link Element}s of a {@link Node} that
     * match specified criteria. No <code>null</code> checking is done of the
     * <code>node</code> argument.
     *
     * @param node
     *        the {@link Node} to enumerate (must not be <code>null</code>)
     * @param useNamespaces
     *        passed to the {@link #matches(Node, boolean, String, String)}
     *        method
     * @param namespaceURI
     *        passed to the {@link #matches(Node, boolean, String, String)}
     *        method
     * @param localName
     *        passed to the {@link #matches(Node, boolean, String, String)}
     *        method
     * @return the {@link Element} children of the given {@link Node} (never
     *         <code>null</code>)
     */
    private static Element[] getChildElementsInternal(
        final Node node,
        final boolean useNamespaces,
        final String namespaceURI,
        final String localName) {
        final NodeList children = node.getChildNodes();
        final List childElements = new ArrayList();

        final int length = children.getLength();
        for (int i = 0; i < length; i++) {
            final Node child = children.item(i);
            if (Node.ELEMENT_NODE == child.getNodeType()) {
                if (matches(child, useNamespaces, namespaceURI, localName)) {
                    childElements.add(child);
                }
            }
        }

        return (Element[]) childElements.toArray(new Element[childElements.size()]);
    }

    /**
     * Helper method to get the first child {@link Element} of a {@link Node}
     * that matches specified criteria. No <code>null</code> checking is done of
     * the <code>node</code> argument.
     *
     * @param node
     *        the {@link Node} to obtain the child from (must not be
     *        <code>null</code>)
     * @param useNamespaces
     *        passed to the {@link #matches(Node, boolean, String, String)}
     *        method
     * @param namespaceURI
     *        passed to the {@link #matches(Node, boolean, String, String)}
     *        method
     * @param localName
     *        passed to the {@link #matches(Node, boolean, String, String)}
     *        method
     * @return the first child {@link Element} of the given {@link Node} that
     *         matches the criteria, or <code>null</code> if none match
     */
    private static Element getFirstChildElementInternal(
        final Node node,
        final boolean useNamespaces,
        final String namespaceURI,
        final String localName) {
        final NodeList children = node.getChildNodes();

        final int length = children.getLength();
        for (int i = 0; i < length; i++) {
            final Node child = children.item(i);
            if (Node.ELEMENT_NODE == child.getNodeType()) {
                if (matches(child, useNamespaces, namespaceURI, localName)) {
                    return (Element) child;
                }
            }
        }

        return null;
    }

    /**
     * Tests whether the given {@link Node} matches the specified name. No
     * <code>null</code> checking is done of the <code>node</code> argument.
     *
     * @param node
     *        the {@link Node} to test (must not be <code>null</code>)
     * @param useNamespaces
     *        <code>true</code> for namespaces mode
     * @param namespaceURI
     *        the namespace URI to match, <code>null</code> to match no
     *        namespace, <code>*</code> to match any namespace (ignored if
     *        <code>useNamespaces</code> is <code>false</code>)
     * @param localName
     *        <code>null</code> or <code>*</code> to match any node, matches the
     *        node name if <code>useNamespaces</code> is <code>false</code>,
     *        matches the local name if <code>useNamespaces</code> is
     *        <code>true</code>
     * @return <code>true</code> if the {@link Node} matches the criteria
     */
    private static boolean matches(
        final Node node,
        final boolean useNamespaces,
        final String namespaceURI,
        final String localName) {
        if (useNamespaces) {
            if (namespaceURI == null && node.getNamespaceURI() != null) {
                return false;
            }

            if (namespaceURI != null && !namespaceURI.equals("*") && !namespaceURI.equals(node.getNamespaceURI())) //$NON-NLS-1$
            {
                return false;
            }
        }

        if (localName == null || localName.equals("*")) //$NON-NLS-1$
        {
            return true;
        }

        if (useNamespaces) {
            return localName.equals(node.getLocalName());
        }

        return localName.equals(node.getNodeName());
    }

}
